/*
 *  
    Copyright (C) 2008  Nathaniel Waisbrot

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package termint.gui;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;

import org.eclipse.swt.widgets.Display;

import termint.util.Pair;

/**
 * A pipe between Java and a local system command
 * Runs as a separate thread because we need to wait for incoming data
 * @author Nathaniel Waisbrot
 *
 */
public class LocalShell {
	/*
	 * Size in bytes of the data-buffer for incoming data
	 */
	private static final int bufferSize = 1024;
	/*
	 * Environmental vars for the command.  $TERM must be set for 'script' to work
	 */
	final String[] env = {"TERM=xterm", "NETHACKOPTIONS=color,disclose:-i -a -v -g -c,!news,time,!legacy"};
	/*
	 * Directory to start in.  'null' causes it to inherit from the parent
	 */
	final File dir = null;

	public class Input {
		OutputStream out;
		private static final String charsetName = "US-ASCII";
		public Input(OutputStream out) {
			this.out = out;
		}
		public Input() {
			this.out = null;
		}
		public void writeData(String s) {
			try {
				writeData(s.getBytes(charsetName));
			} catch (UnsupportedEncodingException e) {
				// DEBUG
				e.printStackTrace();
				throw new RuntimeException("DEBUG: bad charset in LocalShell");
			}
		}
		public void writeData(byte[] buf) {
			try {
				out.write(buf);
				out.flush();
			} catch (IOException e) {
				pipesDie();
			}
		}
		public void setStream(OutputStream o) {
			this.out = o;
		}
	}
	public static class Output implements Runnable {
		InputStream in;
		ByteBuffer shellOut;
		boolean OK;
		private Display wakeMe;
		public Output (InputStream in) {
			this();
			this.in = in;
			this.OK = (in != null);
		}
		public Output () {
			this.shellOut = ByteBuffer.allocate(bufferSize);
			this.in = null;
			this.OK = false;
		}
		public synchronized void setStream(InputStream i) {
			this.in = i;
			this.OK = (in != null);
		}
		private synchronized void bufferData() throws IOException {
			int i = Math.min(in.available(), shellOut.remaining());
			if (i > 0) {
				byte[] b = new byte[1024];
				in.read(b, shellOut.position(), i);
				shellOut.put(b);
				wakeMe.wake();
			}			
		}
		public void run() {
			try {
				while (OK) {
					bufferData();
					Thread.sleep(10);
				}
			} catch (IOException e) {
				System.err.println("IO error: stream closed?");
				e.printStackTrace();
			} catch (InterruptedException|ThreadDeath e) {
				try {
					in.close();
				} catch (IOException e1) {}
				return;
			}
		}
		public synchronized String readString() {
			if (shellOut.position() > 0) {
				shellOut.position(0);
				String s = new String(shellOut.array());
				return s;
			} else {
				return null;
			}
		}
		public synchronized void setWake(Display d) {
			wakeMe = d;
		}
	}
	
	Process shell;
	String[] command;
	Output out;
	Input in;
	ThreadGroup tgroup;
	Thread tout;

	public LocalShell(String command) {
		this(new String[] {command});
	}
	public LocalShell(String[] args) {
		command = args.clone();
		out = new Output();
		in = new Input();
	}
	public Pair<Output,Input> start(Display d) throws IOException {
		tgroup = new ThreadGroup("LocalShell threads");

		// This hook kills off the shell process when the application is killed
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				if (shell != null) {
					shell.destroy();
				}
			}
		});

		shell = Runtime.getRuntime().exec(command, env, dir); 
		out.setStream(shell.getInputStream());
		in.setStream(shell.getOutputStream());
		out.setWake(d);
		tout = new Thread(tgroup, out, "LocalShell output pipe");
		tout.start();
		return new Pair<Output, Input>(out, in);
	}
	
	public ThreadGroup getThreadGroup() {
		return tgroup;
	}
	
	void pipesDie() {
		kill();
		System.err.println("Pipes closed");
	}
	
	/**
	 * Kill both ends of the pipe.  Uses the deprecated Thread.stop(), but this is for cleanup, so I 
	 * don't care about deadlocks.
	 */
	@SuppressWarnings("deprecation")
	public void kill() {
		if (tgroup != null) {
			tgroup.stop();
			tgroup = null;
		}
	}
}
